/*
 Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.

 Licensed under the Apache License, Version 2.0 (the "License").
 You may not use this file except in compliance with the License.
 A copy of the License is located at

 http://aws.amazon.com/apache2.0

 or in the "license" file accompanying this file. This file is distributed
 on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 express or implied. See the License for the specific language governing
 permissions and limitations under the License.
 */

#import "CSPCategory.h"
#import <objc/runtime.h>
#import <CommonCrypto/CommonCryptor.h>
#import <CommonCrypto/CommonDigest.h>
#import <UIKit/UIKit.h>
#import "CSPLogging.h"
#import "CSPMantle.h"

NSString *const CSPiOSSDKVersion = @"2.2.4";

NSString *const CSPDateRFC822DateFormat1 = @"EEE, dd MMM yyyy HH:mm:ss z";
NSString *const CSPDateISO8601DateFormat1 = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
NSString *const CSPDateISO8601DateFormat2 = @"yyyyMMdd'T'HHmmss'Z'";
NSString *const CSPDateISO8601DateFormat3 = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
NSString *const CSPDateShortDateFormat1 = @"yyyyMMdd";

@interface CSPCategory : NSObject

+ (void)loadCategories;

@end

@implementation CSPCategory

+ (void)loadCategories {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
//        awsgzip_loadGZIP();
        cspmtl_loadMTLPredefinedTransformerAdditions();
        cspmtl_loadMTLNSCoding();
    });
}

@end

@implementation NSDate (CSP)

static NSTimeInterval _clockskew = 0.0;

+ (NSDate *)csp_clockSkewFixedDate {
    return [[NSDate date] dateByAddingTimeInterval:-1 * _clockskew];
}

+ (NSDate *)csp_dateFromString:(NSString *)string {
    NSDate *parsedDate = nil;
    NSArray *arrayOfDateFormat = @[CSPDateRFC822DateFormat1,CSPDateISO8601DateFormat1,CSPDateISO8601DateFormat2,CSPDateISO8601DateFormat3];

    for (NSString *dateFormat in arrayOfDateFormat) {
        if (!parsedDate) {
            parsedDate = [NSDate csp_dateFromString:string format:dateFormat];
        } else {
            break;
        }
    }

    return parsedDate;
}

+ (NSDate *)csp_dateFromString:(NSString *)string format:(NSString *)dateFormat {
    NSDateFormatter *dateFormatter = [NSDateFormatter new];
    dateFormatter.timeZone = [NSTimeZone timeZoneWithName:@"GMT"];
    dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
    dateFormatter.dateFormat = dateFormat;

    return [dateFormatter dateFromString:string];
}

- (NSString *)csp_stringValue:(NSString *)dateFormat {
    NSDateFormatter *dateFormatter = [NSDateFormatter new];
    dateFormatter.timeZone = [NSTimeZone timeZoneWithName:@"GMT"];
    dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
    dateFormatter.dateFormat = dateFormat;

    return [dateFormatter stringFromDate:self];
}

+ (void)csp_setRuntimeClockSkew:(NSTimeInterval)clockskew {
    @synchronized(self) {
        _clockskew = clockskew;
    }
}

+ (NSTimeInterval)csp_getRuntimeClockSkew {
    @synchronized(self) {
        return _clockskew;
    }
}

@end

@implementation NSDictionary (CSP)

- (NSDictionary *)csp_removeNullValues {
    return [self csp_recursivelyRemoveNullEntries:self];
}

- (NSDictionary *)csp_recursivelyRemoveNullEntries:(NSDictionary *)inputDictionary {
    
    NSMutableDictionary *resultMutableDictionary = [NSMutableDictionary new];
    
    for (NSString *key in inputDictionary) {
        id value = inputDictionary[key];
        if ([value isEqual:[NSNull null]]) {
            continue;
        }
        if([value isKindOfClass:[NSDictionary class]]) {
            resultMutableDictionary[key] = [self csp_recursivelyRemoveNullEntries:value];
        } else {
            resultMutableDictionary[key] = value;
        }
    }
    return resultMutableDictionary;
}

-(id)csp_objectForCaseInsensitiveKey:(id)aKey {
    for (NSString *key in self.allKeys) {
        if ([key compare:aKey options:NSCaseInsensitiveSearch] == NSOrderedSame) {
            return self[key];
        }
    }
    return  nil;
}

@end

@implementation NSJSONSerialization (CSP)

+ (NSData *)csp_dataWithJSONObject:(id)obj
                           options:(NSJSONWritingOptions)opt
                             error:(NSError **)error {
    if (!obj) {
        return nil;
    }
    if ([NSJSONSerialization isValidJSONObject:obj]) {
        return [NSJSONSerialization dataWithJSONObject:obj
                                               options:opt
                                                 error:error];
    } else {
        NSData *JSONData = [NSJSONSerialization dataWithJSONObject:@[obj]
                                                           options:opt
                                                             error:error];
        NSString *JSONString = [[NSString alloc] initWithData:JSONData
                                                     encoding:NSUTF8StringEncoding];
        if ([JSONString length] > 2) {
            JSONString = [JSONString substringWithRange:NSMakeRange(1, [JSONString length] - 2)];
            return [JSONString dataUsingEncoding:NSUTF8StringEncoding];
        } else {
            return nil;
        }
    }
}

@end

@implementation NSNumber (CSP)

+ (NSNumber *)csp_numberFromString:(NSString *)string {
    NSNumberFormatter *numberFormatter = [NSNumberFormatter new];
    numberFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];

    return [numberFormatter numberFromString:string];
}

@end

@implementation NSObject (CSP)

- (NSDictionary *)csp_properties {
    NSMutableDictionary *propertyDictionary;
    if ([self superclass] != [NSObject class]) {
    	propertyDictionary = [NSMutableDictionary dictionaryWithDictionary:[[self superclass] csp_properties]];
    } else {
    	propertyDictionary = [NSMutableDictionary dictionary];
    }

    unsigned int propertyListCount;
    objc_property_t *properties = class_copyPropertyList([self class], &propertyListCount);
    for(uint32_t i = 0; i < propertyListCount; i++) {
        objc_property_t property = properties[i];
        const char *propertyName = property_getName(property);
        const char *attributes = property_getAttributes(property);
        if(propertyName) {
            NSString *propertyNameString = [NSString stringWithCString:propertyName
                                                              encoding:[NSString defaultCStringEncoding]];
            NSString *attributesString = [NSString stringWithCString:attributes
                                                            encoding:[NSString defaultCStringEncoding]];
            propertyDictionary[propertyNameString] = attributesString;
        }
    }
    free(properties);

    return propertyDictionary;
}

- (void)csp_copyPropertiesFromObject:(NSObject *)object {
    NSDictionary *propertiesToObject = [self csp_properties];
    NSDictionary *propertiesFromObject = [object csp_properties];
    for (NSString *key in [propertiesFromObject allKeys]) {
        if (propertiesToObject[key]) {
            NSString *attributes = [propertiesFromObject valueForKey:key];
            /**
             * If it's not a readonly property
             * Ref. https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW1
             */
            if ([attributes rangeOfString:@",R,"].location == NSNotFound) {
                if (![key isEqualToString:@"uploadProgress"] && ![key isEqualToString:@"downloadProgress"]) {
                    //do not copy progress block since they do not have getter method and they have already been copied via internalRequest. copy it again will result in overwrite the current value to nil.
                    [self setValue:[object valueForKey:key]
                            forKey:key];
                }
            }
        }
    }
}

// 将字典或者数组转化为JSON串
- (NSString *)csp_toJSONString {
    NSError *error = nil;
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:self
                                                       options:NSJSONWritingPrettyPrinted
                                                         error:&error];

    if ([jsonData length] > 0 & error == nil){
        return [[NSString alloc] initWithData:jsonData
                                     encoding:NSUTF8StringEncoding];
    }else{
        return nil;
    }
}

@end

@implementation NSString (CSP)

+ (NSString *)csp_base64md5FromData:(NSData *)data {
    
    if([data length] > UINT32_MAX)
    {
        //The NSData size is too large. The maximum allowable size is UINT32_MAX.
        return nil;
    }
    
    const void    *cStr = [data bytes];
    unsigned char result[CC_MD5_DIGEST_LENGTH];
    
    CC_MD5(cStr, (uint32_t)[data length], result);
    
    NSData *md5 = [[NSData alloc] initWithBytes:result length:CC_MD5_DIGEST_LENGTH];
    return [md5 base64EncodedStringWithOptions:kNilOptions];
}

+ (NSString *)csp_baseUserAgent {
    static NSString *_userAgent = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSString *systemName = [[[UIDevice currentDevice] systemName] stringByReplacingOccurrencesOfString:@" " withString:@"-"];
        NSString *systemVersion = [[UIDevice currentDevice] systemVersion];
        NSString *localeIdentifier = [[NSLocale currentLocale] localeIdentifier];
        _userAgent = [NSString stringWithFormat:@"aws-sdk-iOS/%@ %@/%@ %@", CSPiOSSDKVersion, systemName, systemVersion, localeIdentifier];
    });

    return _userAgent;
}

- (BOOL)csp_isBase64Data {
    if ([self length] % 4 == 0) {
        static NSCharacterSet *invertedBase64CharacterSet = nil;
        if (invertedBase64CharacterSet == nil) {
            invertedBase64CharacterSet = [[NSCharacterSet characterSetWithCharactersInString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="] invertedSet];
        }
        return [self rangeOfCharacterFromSet:invertedBase64CharacterSet
                                     options:NSLiteralSearch].location == NSNotFound;
    }
    return NO;
}

- (NSString *)csp_stringWithURLEncoding {
    return (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
                                                                                 (__bridge CFStringRef)[self csp_decodeURLEncoding],
                                                                                 NULL,
                                                                                 (CFStringRef)@"!*'\();:@&=+$,/?%#[] ",
                                                                                 kCFStringEncodingUTF8));
}

- (NSString *)csp_stringWithURLEncodingPath {
    return (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
                                                                                 (__bridge CFStringRef)[self csp_decodeURLEncoding],
                                                                                 NULL,
                                                                                 (CFStringRef)@"!*'\();:@&=+$,?%#[] ",
                                                                                 kCFStringEncodingUTF8));
}

- (NSString *)csp_stringWithURLEncodingPathWithoutPriorDecoding {
    return (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
                                                                                 (__bridge CFStringRef)self,
                                                                                 NULL,
                                                                                 (CFStringRef)@"!*'\();:@&=+$,?%#[] ",
                                                                                 kCFStringEncodingUTF8));
}

- (NSString *)csp_decodeURLEncoding {
    NSString *result = [self stringByRemovingPercentEncoding];
    return result?result:self;
}

- (NSString *)csp_md5String {
    NSData *dataString = [self dataUsingEncoding:NSUTF8StringEncoding];
    unsigned char digestArray[CC_MD5_DIGEST_LENGTH];
    CC_MD5([dataString bytes], (CC_LONG)[dataString length], digestArray);

    NSMutableString *md5String = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
    for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
        [md5String appendFormat:@"%02x", digestArray[i]];
    }
    return md5String;
}

- (NSString *)csp_md5StringLittleEndian {
    NSData *dataString = [self dataUsingEncoding:NSUTF16LittleEndianStringEncoding];
    unsigned char digestArray[CC_MD5_DIGEST_LENGTH];
    CC_MD5([dataString bytes], (CC_LONG)[dataString length], digestArray);

    NSMutableString *md5String = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
    for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
        [md5String appendFormat:@"%02x", digestArray[i]];
    }
    return md5String;
}

- (BOOL)csp_isDNSBucketName {
    if ([self length] < 3 || [self length] > 63) {
        return NO;
    }

    if ([self hasSuffix:@"-"]) {
        return NO;
    }

    if ([self csp_contains:@"_"]) {
        return NO;
    }

    if ([self csp_contains:@"-."] || [self csp_contains:@".-"]) {
        return NO;
    }

    return [[self lowercaseString] isEqualToString:self];

}

- (BOOL)csp_isVirtualHostedStyleCompliant {
    if (![self csp_isDNSBucketName]) {
        return NO;
    } else {
        return ![self csp_contains:@"."];
    }
}

- (id)csp_parseJSON {
    if ([self length]==0)
        return nil;
    return [NSJSONSerialization JSONObjectWithData:[self dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingAllowFragments error:nil];
}

- (BOOL)csp_contains:(NSString *)searchString {
    NSRange range = [self rangeOfString:searchString];

    return (range.location != NSNotFound);
}

@end

@implementation NSFileManager (CSP)

- (BOOL)csp_atomicallyCopyItemAtURL:(NSURL *)sourceURL
                              toURL:(NSURL *)destinationURL
                     backupItemName:(NSString *)backupItemName
                              error:(NSError **)outError {
    
    NSURL *tempDir = [self URLForDirectory:NSItemReplacementDirectory
                                  inDomain:NSUserDomainMask
                         appropriateForURL:destinationURL
                                    create:YES
                                     error:outError];
    
    if (!tempDir) return NO;
    
    NSURL *tempURL = [tempDir URLByAppendingPathComponent:[destinationURL lastPathComponent]];
    
    BOOL result = [self copyItemAtURL:sourceURL toURL:tempURL error:outError];
    if (result) {
        
        result = [self replaceItemAtURL:destinationURL
                          withItemAtURL:tempURL
                         backupItemName:backupItemName
                                options:NSFileManagerItemReplacementUsingNewMetadataOnly
                       resultingItemURL:nil
                                  error:outError];
        if (!result) {
            if (backupItemName) {
                NSURL *backupItemURL = [[destinationURL URLByDeletingLastPathComponent] URLByAppendingPathComponent:backupItemName];
                NSError *error = nil;
                BOOL success = [self replaceItemAtURL:destinationURL
                                        withItemAtURL:backupItemURL
                                       backupItemName:nil
                                              options:NSFileManagerItemReplacementUsingNewMetadataOnly
                                     resultingItemURL:nil error:&error];
                if (!success) {
                    if (error) {
                        CSPLogError(@"Failed to move backupItemURL directory(%@) to destinationURL(%@): %@" ,backupItemURL,destinationURL,error);
                    }
                    if ([self fileExistsAtPath:[destinationURL path]]) {
                        NSError *removeError = nil;
                        if (![self removeItemAtURL:destinationURL error:&removeError]) {
                            CSPLogError(@"Failed to remove destinationURL(%@): %@",destinationURL,removeError);
                        }
                    }
                    
                }
            }
        }
    }
    
    NSError *error;
    if (![self removeItemAtURL:tempDir error:&error])
    {
        CSPLogError(@"Failed to remove temp(%@) directory after atomic copy: %@",tempDir,error);
    }
    
    return result;
}

@end
